﻿/*
* Copyright (c) 2010-2012 Tesla Engine Group
* 
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* 
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
* 
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/


using System;
using System.Collections.Generic;
using Tesla.Core;
using Tesla.Graphics;
using D3D = SlimDX.Direct3D10;
using D3DC = SlimDX.D3DCompiler;

namespace Tesla.Direct3D10.Graphics {
    /// <summary>
    /// Input layout map. Each shader has a layout map which binds a layout key with a specific input layout that is
    /// specific to that shader, layouts bound to a shader can be re-used by that shader, or shaders cloned from it (this layoutmap is shared
    /// between all instances).
    /// </summary>
    internal sealed class InputLayoutMap : IDisposable {

        private D3D.Device _device;
        private D3DC.ShaderSignature _sig;
        private Dictionary<int, D3D.InputLayout> _layoutMap;
        private int[] _semanticIndexCount;
        private bool _isDisposed;

        public bool IsDisposed {
            get {
                return _isDisposed;
            }
        }

        public InputLayoutMap(D3D10Renderer renderer, D3DC.ShaderSignature shaderSignature) {
            _device = renderer.GraphicsDevice;
            _sig = shaderSignature;
            _layoutMap = new Dictionary<int, D3D.InputLayout>();
            _semanticIndexCount = new int[Enum.GetValues(typeof(VertexSemantic)).Length];
            _isDisposed = false;
            renderer.Disposing += new EventHandler(delegate(object o, EventArgs args) {
                Dispose();
            });
        }

        public D3D.InputLayout CreateLayout(VertexDeclaration declaration) {
            int key = declaration.GetHashCode();
            D3D.InputLayout layout;
            if(!_layoutMap.TryGetValue(key, out layout)) {
                try {
                    D3D.InputElement[] elements = new D3D.InputElement[declaration.ElementCount];
                    for(int i = 0; i < elements.Length; i++) {
                        elements[i] = GenerateInputElement(declaration[i], 0, 0);
                    }
                    layout = new D3D.InputLayout(_device, _sig, elements);
                } catch(Exception e) {
                    throw new TeslaException("Error setting vertex declaration, it does not match the shader's input signature. Either there is an invalid element, or the declaration does not have enough elements.", e);
                }
                _layoutMap.Add(key, layout);
            }
            return layout;
        }

        public D3D.InputLayout GetOrCreateLayout(VertexBufferBinding vbuffer) {
            VertexDeclaration decl = vbuffer.VertexBuffer.VertexDeclaration;
            int key = decl.GetHashCode();
            D3D.InputLayout layout;
            if(!_layoutMap.TryGetValue(key, out layout)) {
                try {
                    D3D.InputElement[] elements = new D3D.InputElement[decl.ElementCount];
                    for(int i = 0; i < elements.Length; i++) {
                        elements[i] = GenerateInputElement(decl[i], 0, 0);
                    }
                    layout = new D3D.InputLayout(_device, _sig, elements);
                } catch(Exception e) {
                    throw new TeslaException("Error setting vertex declaration, it does not match the shader's input signature. Either there is an invalid element, or the declaration does not have enough elements.", e);
                }
                _layoutMap.Add(key, layout);
            }
            return layout;
        }

        public D3D.InputLayout GetOrCreateLayout(VertexBufferBinding[] vbuffers, int numBuffers) {
            //Take care of the trivial cases
            if(numBuffers == 1) {
                return GetOrCreateLayout(vbuffers[0]);
            } else if(numBuffers == 0) {
                throw new TeslaException("Error creating input layout - cannot draw!");
            }

            //Clear semantic index array
            ClearSemanticIndices();

            //Make sure we aren't trying to do too much
            if(numBuffers >= vbuffers.Length) {
                throw new ArgumentOutOfRangeException("numBuffers", "Number of vertex declarations to merge is greater than number of vertex buffers given.");
            }

            //Do we have an input layout that matches?
            int key = GenerateKey(vbuffers, numBuffers);
            D3D.InputLayout layout;
            if(!_layoutMap.TryGetValue(key, out layout)) {
                //If not, need to loop over each vertex buffer and create a layout from each declaration
                try {
                    D3D.InputElement[] elements = new D3D.InputElement[GetVertexElementCount(vbuffers, numBuffers)];
                    int index = 0;
                    for(int i = 0; i < numBuffers; i++) {
                        VertexBufferBinding binding = vbuffers[i];
                        VertexDeclaration decl = binding.VertexBuffer.VertexDeclaration;
                        int offset = 0; //Every new stream, reset offset
                        for(int j = 0; j < decl.ElementCount; j++) {
                            VertexElement element = decl[j];
                            element.SemanticIndex = IncrementSemanticIndex(element.SemanticName);
                            element.Offset = offset;
                            elements[index++] = GenerateInputElement(element, i, binding.InstanceFrequency);
                            offset += VertexDeclaration.GetVertexElementSize(element.Format);
                        }
                    }
                    layout = new D3D.InputLayout(_device, _sig, elements);
                } catch(Exception e) {
                    throw new TeslaException("Error setting vertex declaration, it does not match the shader's input signature. Either there is an invalid element, or the declaration does not have enough elements.", e);
                }
                //If we're good, add it to the map
                _layoutMap.Add(key, layout);
            }
            return layout;
        }

        private D3D.InputElement GenerateInputElement(VertexElement element, int streamIndex, int instanceFrequency) {
            return new D3D.InputElement(D3D10Helper.ToD3DVertexSemantic(element.SemanticName),
                    element.SemanticIndex,
                    D3D10Helper.ToD3DVertexFormat(element.Format),
                    element.Offset,
                    streamIndex,
                    (instanceFrequency > 0) ? D3D.InputClassification.PerInstanceData : D3D.InputClassification.PerVertexData,
                    instanceFrequency);
        }

        private int GetVertexElementCount(VertexBufferBinding[] vbuffers, int numBuffers) {
            VertexDeclaration decl;
            int count = 0;
            for(int i = 0; i < numBuffers; i++) {
                decl = vbuffers[i].VertexBuffer.VertexDeclaration;
                count += decl.ElementCount;
            }
            return count;
        }

        private void ClearSemanticIndices() {
            Array.Clear(_semanticIndexCount, 0, _semanticIndexCount.Length);
        }

        private int IncrementSemanticIndex(VertexSemantic semantic) {
            int arrayIndex = (int) semantic;
            int arrayValue = _semanticIndexCount[arrayIndex];
            _semanticIndexCount[arrayIndex] = arrayValue + 1;
            return arrayValue;
        }

        private int GenerateKey(VertexBufferBinding[] vbuffers, int numBuffers) {
            int key = 0;
            VertexBuffer vb = vbuffers[0].VertexBuffer;
            if(vb == null) {
                throw new ArgumentNullException("Vertex buffer set to slot 0 does not exist.");
            }
            key = vb.VertexDeclaration.GetHashCode();
            for(int i = 1; i < numBuffers; i++) {
                vb = vbuffers[1].VertexBuffer;
                if(vb == null) {
                    throw new ArgumentNullException(String.Format("Vertex buffer set to slot {0} does not exist.", i));
                }
                key = ((key << 3) - key) + vb.VertexDeclaration.GetHashCode();
            }

            return key;
        }

        ~InputLayoutMap() {
            Dispose(false);
        }

        public void Dispose() {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        private void Dispose(bool disposing) {
            if(!_isDisposed) {
                if(disposing) {
                    if(_sig != null) {
                        _sig.Dispose();
                        _sig = null;
                    }
                    if(_layoutMap != null) {
                        foreach(KeyValuePair<int, D3D.InputLayout> kv in _layoutMap) {
                            if(kv.Value != null) {
                                kv.Value.Dispose();
                            }
                        }
                        _layoutMap.Clear();
                        _layoutMap = null;
                    }
                }
            }
            _isDisposed = true;
        }
    }
}
